/*
* Vulkan glTF model and texture loading class based on tinyglTF (https://github.com/syoyo/tinygltf)
*
* Copyright (C) 2018 by Sascha Willems - www.saschawillems.de
*
* This code is licensed under the MIT license (MIT) (http://opensource.org/licenses/MIT)
*/

#pragma once

#include <stdlib.h>
#include <string>
#include <fstream>
#include <sstream>
#include <vector>

#include "vulkan/vulkan.h"
#include "VulkanDevice.hpp"

#define GLM_FORCE_RADIANS
#define GLM_FORCE_DEPTH_ZERO_TO_ONE
#include <glm/glm.hpp>
#include <glm/gtc/matrix_transform.hpp>
#include <glm/gtc/type_ptr.hpp>
#include <gli/gli.hpp>
#include <glm/gtx/string_cast.hpp>

// ERROR is already defined in wingdi.h and collides with a define in the Draco headers
#if defined(_WIN32) && defined(ERROR) && defined(TINYGLTF_ENABLE_DRACO) 
#undef ERROR
#pragma message ("ERROR constant already defined, undefining")
#endif

// #define TINYGLTF_IMPLEMENTATION
#define TINYGLTF_NO_STB_IMAGE_WRITE
#define STB_IMAGE_IMPLEMENTATION
#define STBI_MSC_SECURE_CRT
#include "tiny_gltf.h"

#if defined(__ANDROID__)
#include <android/asset_manager.h>
#endif

// Changing this value here also requires changing it in the vertex shader
#define MAX_NUM_JOINTS 128u

namespace vkglTF
{
    static void Dump(const tinygltf::Model &model);

    struct Node;

    struct BoundingBox {
        glm::vec3 min;
        glm::vec3 max;
        bool valid = false;
        BoundingBox() {};
        BoundingBox(glm::vec3 min, glm::vec3 max) : min(min), max(max) {}
        BoundingBox getAABB(glm::mat4 m) {
            glm::vec3 min = glm::vec3(m[3]);
            glm::vec3 max = min;
            glm::vec3 v0, v1;
            
            glm::vec3 right = glm::vec3(m[0]);
            v0 = right * this->min.x;
            v1 = right * this->max.x;
            min += glm::min(v0, v1);
            max += glm::max(v0, v1);

            glm::vec3 up = glm::vec3(m[1]);
            v0 = up * this->min.y;
            v1 = up * this->max.y;
            min += glm::min(v0, v1);
            max += glm::max(v0, v1);

            glm::vec3 back = glm::vec3(m[2]);
            v0 = back * this->min.z;
            v1 = back * this->max.z;
            min += glm::min(v0, v1);
            max += glm::max(v0, v1);

            return BoundingBox(min, max);
        }
    };

    /*
        glTF texture sampler        
    */
    struct TextureSampler {
        VkFilter magFilter;
        VkFilter minFilter;
        VkSamplerAddressMode addressModeU;
        VkSamplerAddressMode addressModeV;
        VkSamplerAddressMode addressModeW;
    };

    /*
        glTF texture loading class
    */
    struct Texture {
        vks::VulkanDevice *device;
        VkImage image;
        VkImageLayout imageLayout;
        VkDeviceMemory deviceMemory;
        VkImageView view;
        uint32_t width, height;
        uint32_t mipLevels;
        uint32_t layerCount;
        VkDescriptorImageInfo descriptor;
        VkSampler sampler;

        void updateDescriptor()
        {
            descriptor.sampler = sampler;
            descriptor.imageView = view;
            descriptor.imageLayout = imageLayout;
        }

        void destroy()
        {
            vkDestroyImageView(device->logicalDevice, view, nullptr);
            vkDestroyImage(device->logicalDevice, image, nullptr);
            vkFreeMemory(device->logicalDevice, deviceMemory, nullptr);
            vkDestroySampler(device->logicalDevice, sampler, nullptr);
        }

        /*
            Load a texture from a glTF image (stored as vector of chars loaded via stb_image)
            Also generates the mip chain as glTF images are stored as jpg or png without any mips
        */
        void fromglTfImage(tinygltf::Image &gltfimage, TextureSampler textureSampler, vks::VulkanDevice *device, VkQueue copyQueue)
        {
            this->device = device;

            unsigned char* buffer = nullptr;
            VkDeviceSize bufferSize = 0;
            bool deleteBuffer = false;
            if (gltfimage.component == 3) {
                // Most devices don't support RGB only on Vulkan so convert if necessary
                // TODO: Check actual format support and transform only if required
                bufferSize = gltfimage.width * gltfimage.height * 4;
                buffer = new unsigned char[bufferSize];
                unsigned char* rgba = buffer;
                unsigned char* rgb = &gltfimage.image[0];
                for (int32_t i = 0; i< gltfimage.width * gltfimage.height; ++i) {
                    for (int32_t j = 0; j < 3; ++j) {
                        rgba[j] = rgb[j];
                    }
                    rgba += 4;
                    rgb += 3;
                }
                deleteBuffer = true;
            }
            else {
                buffer = &gltfimage.image[0];
                bufferSize = gltfimage.image.size();
            }

            VkFormat format = VK_FORMAT_R8G8B8A8_UNORM;

            VkFormatProperties formatProperties;

            width = gltfimage.width;
            height = gltfimage.height;
            mipLevels = static_cast<uint32_t>(floor(log2(std::max(width, height))) + 1.0);

            vkGetPhysicalDeviceFormatProperties(device->physicalDevice, format, &formatProperties);
            assert(formatProperties.optimalTilingFeatures & VK_FORMAT_FEATURE_BLIT_SRC_BIT);
            assert(formatProperties.optimalTilingFeatures & VK_FORMAT_FEATURE_BLIT_DST_BIT);

            VkMemoryAllocateInfo memAllocInfo{};
            memAllocInfo.sType = VK_STRUCTURE_TYPE_MEMORY_ALLOCATE_INFO;
            VkMemoryRequirements memReqs{};

            VkBuffer stagingBuffer;
            VkDeviceMemory stagingMemory;

            VkBufferCreateInfo bufferCreateInfo{};
            bufferCreateInfo.sType = VK_STRUCTURE_TYPE_BUFFER_CREATE_INFO;
            bufferCreateInfo.size = bufferSize;
            bufferCreateInfo.usage = VK_BUFFER_USAGE_TRANSFER_SRC_BIT;
            bufferCreateInfo.sharingMode = VK_SHARING_MODE_EXCLUSIVE;
            VK_CHECK_RESULT(vkCreateBuffer(device->logicalDevice, &bufferCreateInfo, nullptr, &stagingBuffer));
            vkGetBufferMemoryRequirements(device->logicalDevice, stagingBuffer, &memReqs);
            memAllocInfo.allocationSize = memReqs.size;
            memAllocInfo.memoryTypeIndex = device->getMemoryType(memReqs.memoryTypeBits, VK_MEMORY_PROPERTY_HOST_VISIBLE_BIT | VK_MEMORY_PROPERTY_HOST_COHERENT_BIT);
            VK_CHECK_RESULT(vkAllocateMemory(device->logicalDevice, &memAllocInfo, nullptr, &stagingMemory));
            VK_CHECK_RESULT(vkBindBufferMemory(device->logicalDevice, stagingBuffer, stagingMemory, 0));

            uint8_t *data;
            VK_CHECK_RESULT(vkMapMemory(device->logicalDevice, stagingMemory, 0, memReqs.size, 0, (void **)&data));
            memcpy(data, buffer, bufferSize);
            vkUnmapMemory(device->logicalDevice, stagingMemory);

            VkImageCreateInfo imageCreateInfo{};
            imageCreateInfo.sType = VK_STRUCTURE_TYPE_IMAGE_CREATE_INFO;
            imageCreateInfo.imageType = VK_IMAGE_TYPE_2D;
            imageCreateInfo.format = format;
            imageCreateInfo.mipLevels = mipLevels;
            imageCreateInfo.arrayLayers = 1;
            imageCreateInfo.samples = VK_SAMPLE_COUNT_1_BIT;
            imageCreateInfo.tiling = VK_IMAGE_TILING_OPTIMAL;
            imageCreateInfo.usage = VK_IMAGE_USAGE_SAMPLED_BIT;
            imageCreateInfo.sharingMode = VK_SHARING_MODE_EXCLUSIVE;
            imageCreateInfo.initialLayout = VK_IMAGE_LAYOUT_UNDEFINED;
            imageCreateInfo.extent = { width, height, 1 };
            imageCreateInfo.usage = VK_IMAGE_USAGE_TRANSFER_DST_BIT | VK_IMAGE_USAGE_TRANSFER_SRC_BIT | VK_IMAGE_USAGE_SAMPLED_BIT;
            VK_CHECK_RESULT(vkCreateImage(device->logicalDevice, &imageCreateInfo, nullptr, &image));
            vkGetImageMemoryRequirements(device->logicalDevice, image, &memReqs);
            memAllocInfo.allocationSize = memReqs.size;
            memAllocInfo.memoryTypeIndex = device->getMemoryType(memReqs.memoryTypeBits, VK_MEMORY_PROPERTY_DEVICE_LOCAL_BIT);
            VK_CHECK_RESULT(vkAllocateMemory(device->logicalDevice, &memAllocInfo, nullptr, &deviceMemory));
            VK_CHECK_RESULT(vkBindImageMemory(device->logicalDevice, image, deviceMemory, 0));

            VkCommandBuffer copyCmd = device->createCommandBuffer(VK_COMMAND_BUFFER_LEVEL_PRIMARY, true);

            VkImageSubresourceRange subresourceRange = {};
            subresourceRange.aspectMask = VK_IMAGE_ASPECT_COLOR_BIT;
            subresourceRange.levelCount = 1;
            subresourceRange.layerCount = 1;

            {
                VkImageMemoryBarrier imageMemoryBarrier{};
                imageMemoryBarrier.sType = VK_STRUCTURE_TYPE_IMAGE_MEMORY_BARRIER;
                imageMemoryBarrier.oldLayout = VK_IMAGE_LAYOUT_UNDEFINED;
                imageMemoryBarrier.newLayout = VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL;
                imageMemoryBarrier.srcAccessMask = 0;
                imageMemoryBarrier.dstAccessMask = VK_ACCESS_TRANSFER_WRITE_BIT;
                imageMemoryBarrier.image = image;
                imageMemoryBarrier.subresourceRange = subresourceRange;
                vkCmdPipelineBarrier(copyCmd, VK_PIPELINE_STAGE_ALL_COMMANDS_BIT, VK_PIPELINE_STAGE_ALL_COMMANDS_BIT, 0, 0, nullptr, 0, nullptr, 1, &imageMemoryBarrier);
            }

            VkBufferImageCopy bufferCopyRegion = {};
            bufferCopyRegion.imageSubresource.aspectMask = VK_IMAGE_ASPECT_COLOR_BIT;
            bufferCopyRegion.imageSubresource.mipLevel = 0;
            bufferCopyRegion.imageSubresource.baseArrayLayer = 0;
            bufferCopyRegion.imageSubresource.layerCount = 1;
            bufferCopyRegion.imageExtent.width = width;
            bufferCopyRegion.imageExtent.height = height;
            bufferCopyRegion.imageExtent.depth = 1;

            vkCmdCopyBufferToImage(copyCmd, stagingBuffer, image, VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, 1, &bufferCopyRegion);

            {
                VkImageMemoryBarrier imageMemoryBarrier{};
                imageMemoryBarrier.sType = VK_STRUCTURE_TYPE_IMAGE_MEMORY_BARRIER;
                imageMemoryBarrier.oldLayout = VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL;
                imageMemoryBarrier.newLayout = VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL;
                imageMemoryBarrier.srcAccessMask = VK_ACCESS_TRANSFER_WRITE_BIT;
                imageMemoryBarrier.dstAccessMask = VK_ACCESS_TRANSFER_READ_BIT;
                imageMemoryBarrier.image = image;
                imageMemoryBarrier.subresourceRange = subresourceRange;
                vkCmdPipelineBarrier(copyCmd, VK_PIPELINE_STAGE_ALL_COMMANDS_BIT, VK_PIPELINE_STAGE_ALL_COMMANDS_BIT, 0, 0, nullptr, 0, nullptr, 1, &imageMemoryBarrier);
            }

            device->flushCommandBuffer(copyCmd, copyQueue, true);

            vkFreeMemory(device->logicalDevice, stagingMemory, nullptr);
            vkDestroyBuffer(device->logicalDevice, stagingBuffer, nullptr);

            // Generate the mip chain (glTF uses jpg and png, so we need to create this manually)
            VkCommandBuffer blitCmd = device->createCommandBuffer(VK_COMMAND_BUFFER_LEVEL_PRIMARY, true);
            for (uint32_t i = 1; i < mipLevels; i++) {
                VkImageBlit imageBlit{};

                imageBlit.srcSubresource.aspectMask = VK_IMAGE_ASPECT_COLOR_BIT;
                imageBlit.srcSubresource.layerCount = 1;
                imageBlit.srcSubresource.mipLevel = i - 1;
                imageBlit.srcOffsets[1].x = int32_t(width >> (i - 1));
                imageBlit.srcOffsets[1].y = int32_t(height >> (i - 1));
                imageBlit.srcOffsets[1].z = 1;

                imageBlit.dstSubresource.aspectMask = VK_IMAGE_ASPECT_COLOR_BIT;
                imageBlit.dstSubresource.layerCount = 1;
                imageBlit.dstSubresource.mipLevel = i;
                imageBlit.dstOffsets[1].x = int32_t(width >> i);
                imageBlit.dstOffsets[1].y = int32_t(height >> i);
                imageBlit.dstOffsets[1].z = 1;

                VkImageSubresourceRange mipSubRange = {};
                mipSubRange.aspectMask = VK_IMAGE_ASPECT_COLOR_BIT;
                mipSubRange.baseMipLevel = i;
                mipSubRange.levelCount = 1;
                mipSubRange.layerCount = 1;

                {
                    VkImageMemoryBarrier imageMemoryBarrier{};
                    imageMemoryBarrier.sType = VK_STRUCTURE_TYPE_IMAGE_MEMORY_BARRIER;
                    imageMemoryBarrier.oldLayout = VK_IMAGE_LAYOUT_UNDEFINED;
                    imageMemoryBarrier.newLayout = VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL;
                    imageMemoryBarrier.srcAccessMask = 0;
                    imageMemoryBarrier.dstAccessMask = VK_ACCESS_TRANSFER_WRITE_BIT;
                    imageMemoryBarrier.image = image;
                    imageMemoryBarrier.subresourceRange = mipSubRange;
                    vkCmdPipelineBarrier(blitCmd, VK_PIPELINE_STAGE_TRANSFER_BIT, VK_PIPELINE_STAGE_TRANSFER_BIT, 0, 0, nullptr, 0, nullptr, 1, &imageMemoryBarrier);
                }

                vkCmdBlitImage(blitCmd, image, VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL, image, VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, 1, &imageBlit, VK_FILTER_LINEAR);

                {
                    VkImageMemoryBarrier imageMemoryBarrier{};
                    imageMemoryBarrier.sType = VK_STRUCTURE_TYPE_IMAGE_MEMORY_BARRIER;
                    imageMemoryBarrier.oldLayout = VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL;
                    imageMemoryBarrier.newLayout = VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL;
                    imageMemoryBarrier.srcAccessMask = VK_ACCESS_TRANSFER_WRITE_BIT;
                    imageMemoryBarrier.dstAccessMask = VK_ACCESS_TRANSFER_READ_BIT;
                    imageMemoryBarrier.image = image;
                    imageMemoryBarrier.subresourceRange = mipSubRange;
                    vkCmdPipelineBarrier(blitCmd, VK_PIPELINE_STAGE_TRANSFER_BIT, VK_PIPELINE_STAGE_TRANSFER_BIT, 0, 0, nullptr, 0, nullptr, 1, &imageMemoryBarrier);
                }
            }

            subresourceRange.levelCount = mipLevels;
            imageLayout = VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL;

            {
                VkImageMemoryBarrier imageMemoryBarrier{};
                imageMemoryBarrier.sType = VK_STRUCTURE_TYPE_IMAGE_MEMORY_BARRIER;
                imageMemoryBarrier.oldLayout = VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL;
                imageMemoryBarrier.newLayout = VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL;
                imageMemoryBarrier.srcAccessMask = VK_ACCESS_TRANSFER_WRITE_BIT;
                imageMemoryBarrier.dstAccessMask = VK_ACCESS_TRANSFER_READ_BIT;
                imageMemoryBarrier.image = image;
                imageMemoryBarrier.subresourceRange = subresourceRange;
                vkCmdPipelineBarrier(blitCmd, VK_PIPELINE_STAGE_ALL_COMMANDS_BIT, VK_PIPELINE_STAGE_ALL_COMMANDS_BIT, 0, 0, nullptr, 0, nullptr, 1, &imageMemoryBarrier);
            }

            device->flushCommandBuffer(blitCmd, copyQueue, true);

            VkSamplerCreateInfo samplerInfo{};
            samplerInfo.sType = VK_STRUCTURE_TYPE_SAMPLER_CREATE_INFO;
            samplerInfo.magFilter = textureSampler.magFilter;
            samplerInfo.minFilter = textureSampler.minFilter;
            samplerInfo.mipmapMode = VK_SAMPLER_MIPMAP_MODE_LINEAR;
            samplerInfo.addressModeU = textureSampler.addressModeU;
            samplerInfo.addressModeV = textureSampler.addressModeV;
            samplerInfo.addressModeW = textureSampler.addressModeW;
            samplerInfo.compareOp = VK_COMPARE_OP_NEVER;
            samplerInfo.borderColor = VK_BORDER_COLOR_FLOAT_OPAQUE_WHITE;
            samplerInfo.maxAnisotropy = 1.0;
            samplerInfo.anisotropyEnable = VK_FALSE;
            samplerInfo.maxLod = (float)mipLevels;
            samplerInfo.maxAnisotropy = 8.0f;
            samplerInfo.anisotropyEnable = VK_TRUE;
            VK_CHECK_RESULT(vkCreateSampler(device->logicalDevice, &samplerInfo, nullptr, &sampler));

            VkImageViewCreateInfo viewInfo{};
            viewInfo.sType = VK_STRUCTURE_TYPE_IMAGE_VIEW_CREATE_INFO;
            viewInfo.image = image;
            viewInfo.viewType = VK_IMAGE_VIEW_TYPE_2D;
            viewInfo.format = format;
            viewInfo.components = { VK_COMPONENT_SWIZZLE_R, VK_COMPONENT_SWIZZLE_G, VK_COMPONENT_SWIZZLE_B, VK_COMPONENT_SWIZZLE_A };
            viewInfo.subresourceRange.aspectMask = VK_IMAGE_ASPECT_COLOR_BIT;
            viewInfo.subresourceRange.layerCount = 1;
            viewInfo.subresourceRange.levelCount = mipLevels;
            VK_CHECK_RESULT(vkCreateImageView(device->logicalDevice, &viewInfo, nullptr, &view));

            descriptor.sampler = sampler;
            descriptor.imageView = view;
            descriptor.imageLayout = imageLayout;

            if (deleteBuffer)
                delete[] buffer;

        }
    };

    /*
        glTF material class
    */
    struct Material {        
        enum AlphaMode{ ALPHAMODE_OPAQUE, ALPHAMODE_MASK, ALPHAMODE_BLEND };
        AlphaMode alphaMode = ALPHAMODE_OPAQUE;
        float alphaCutoff = 1.0f;
        float metallicFactor = 1.0f;
        float roughnessFactor = 1.0f;
        glm::vec4 baseColorFactor = glm::vec4(1.0f);
        glm::vec4 emissiveFactor = glm::vec4(1.0f);
        vkglTF::Texture *baseColorTexture;
        vkglTF::Texture *metallicRoughnessTexture;
        vkglTF::Texture *normalTexture;
        vkglTF::Texture *occlusionTexture;
        vkglTF::Texture *emissiveTexture;
        struct TexCoordSets {
            uint8_t baseColor = 0;
            uint8_t metallicRoughness = 0;
            uint8_t specularGlossiness = 0;
            uint8_t normal = 0;
            uint8_t occlusion = 0;
            uint8_t emissive = 0;
        } texCoordSets;
        struct Extension {
            vkglTF::Texture *specularGlossinessTexture;
            vkglTF::Texture *diffuseTexture;
            glm::vec4 diffuseFactor = glm::vec4(1.0f);
            glm::vec3 specularFactor = glm::vec3(0.0f);
        } extension;
        struct PbrWorkflows {
            bool metallicRoughness = true;
            bool specularGlossiness = false;
        } pbrWorkflows;
        VkDescriptorSet descriptorSet = VK_NULL_HANDLE;
    };

    /*
        glTF primitive
    */
    struct Primitive {
        uint32_t firstIndex;
        uint32_t indexCount;
        uint32_t vertexCount;
        Material &material;
        bool hasIndices;

        BoundingBox bb;

        Primitive(uint32_t firstIndex, uint32_t indexCount, uint32_t vertexCount, Material &material) : firstIndex(firstIndex), indexCount(indexCount), vertexCount(vertexCount), material(material) {
            hasIndices = indexCount > 0;
        };

        void setBoundingBox(glm::vec3 min, glm::vec3 max) {
            bb.min = min;
            bb.max = max;
            bb.valid = true;
        }
    };

    /*
        glTF mesh
    */
    struct Mesh {
        vks::VulkanDevice *device;

        std::vector<Primitive*> primitives;

        BoundingBox bb;
        BoundingBox aabb;

        struct UniformBuffer {
            VkBuffer buffer;
            VkDeviceMemory memory;
            VkDescriptorBufferInfo descriptor;
            VkDescriptorSet descriptorSet = VK_NULL_HANDLE;
            void *mapped;
        } uniformBuffer;

        struct UniformBlock {
            glm::mat4 matrix;
            glm::mat4 jointMatrix[MAX_NUM_JOINTS]{};
            float jointcount{ 0 };
        } uniformBlock;

        Mesh(vks::VulkanDevice *device, glm::mat4 matrix) {
            this->device = device;
            this->uniformBlock.matrix = matrix;
            VK_CHECK_RESULT(device->createBuffer(
                VK_BUFFER_USAGE_UNIFORM_BUFFER_BIT,
                VK_MEMORY_PROPERTY_HOST_VISIBLE_BIT | VK_MEMORY_PROPERTY_HOST_COHERENT_BIT,
                sizeof(uniformBlock),
                &uniformBuffer.buffer,
                &uniformBuffer.memory,
                &uniformBlock));
            VK_CHECK_RESULT(vkMapMemory(device->logicalDevice, uniformBuffer.memory, 0, sizeof(uniformBlock), 0, &uniformBuffer.mapped));
            uniformBuffer.descriptor = { uniformBuffer.buffer, 0, sizeof(uniformBlock) };
        };

        ~Mesh() {
            vkDestroyBuffer(device->logicalDevice, uniformBuffer.buffer, nullptr);
            vkFreeMemory(device->logicalDevice, uniformBuffer.memory, nullptr);
            for (Primitive* p : primitives)
                delete p;
        }

        void setBoundingBox(glm::vec3 min, glm::vec3 max) {
            bb.min = min;
            bb.max = max;
            bb.valid = true;
        }
    };

    /*
        glTF skin
    */
    struct Skin {
        std::string name;
        Node *skeletonRoot = nullptr;
        std::vector<glm::mat4> inverseBindMatrices;
        std::vector<Node*> joints;
    };

    /*
        glTF node
    */
    struct Node {
        Node *parent;
        uint32_t index;
        std::vector<Node*> children;
        glm::mat4 matrix;
        std::string name;
        Mesh *mesh;
        Skin *skin;
        int32_t skinIndex = -1;
        glm::vec3 translation{};
        glm::vec3 scale{ 1.0f };
        glm::quat rotation{};
        BoundingBox bvh;
        BoundingBox aabb;

        glm::mat4 localMatrix() {
            return glm::translate(glm::mat4(1.0f), translation) * glm::mat4(rotation) * glm::scale(glm::mat4(1.0f), scale) * matrix;
        }

        glm::mat4 getMatrix() {
            glm::mat4 m = localMatrix();
            vkglTF::Node *p = parent;
            while (p) {
                m = p->localMatrix() * m;
                p = p->parent;
            }
            return m;
        }

        void update() {
            if (mesh) {
                glm::mat4 m = getMatrix();
                if (skin) {
                    mesh->uniformBlock.matrix = m;
                    // Update join matrices
                    glm::mat4 inverseTransform = glm::inverse(m);
                    size_t numJoints = std::min((uint32_t)skin->joints.size(), MAX_NUM_JOINTS);
                    for (size_t i = 0; i < numJoints; i++) {
                        vkglTF::Node *jointNode = skin->joints[i];
                        glm::mat4 jointMat = jointNode->getMatrix() * skin->inverseBindMatrices[i];
                        jointMat = inverseTransform * jointMat;
                        mesh->uniformBlock.jointMatrix[i] = jointMat;
                    }
                    mesh->uniformBlock.jointcount = (float)numJoints;
                    memcpy(mesh->uniformBuffer.mapped, &mesh->uniformBlock, sizeof(mesh->uniformBlock));
                } else {
                    memcpy(mesh->uniformBuffer.mapped, &m, sizeof(glm::mat4));
                }
            }

            for (auto& child : children) {
                child->update();
            }
        }

        ~Node() {
            if (mesh) {
                delete mesh;
            }
            for (auto& child : children) {
                delete child;
            }
        }
    };

    /*
        glTF animation channel
    */
    struct AnimationChannel {
        enum PathType { TRANSLATION, ROTATION, SCALE };
        PathType path;
        Node *node;
        uint32_t samplerIndex;
    };

    /*
        glTF animation sampler
    */
    struct AnimationSampler {
        enum InterpolationType { LINEAR, STEP, CUBICSPLINE };
        InterpolationType interpolation;
        std::vector<float> inputs;
        std::vector<glm::vec4> outputsVec4;
    };

    /*
        glTF animation
    */
    struct Animation {
        std::string name;
        std::vector<AnimationSampler> samplers;
        std::vector<AnimationChannel> channels;
        float start = std::numeric_limits<float>::max();
        float end = std::numeric_limits<float>::min();
    };

    /*
        glTF model loading and rendering class
    */
    struct Model {

        std::string name;

        vks::VulkanDevice *device;

        struct Vertex {
            glm::vec3 pos;
            glm::vec3 normal;
            glm::vec2 uv0;
            glm::vec2 uv1;
            glm::vec4 joint0;
            glm::vec4 weight0;
        };

        struct Vertices {
            VkBuffer buffer = VK_NULL_HANDLE;
            VkDeviceMemory memory;
        } vertices;
        struct Indices {
            int count;
            VkBuffer buffer = VK_NULL_HANDLE;
            VkDeviceMemory memory;
        } indices;

        glm::mat4 aabb;

        std::vector<Node*> nodes;
        std::vector<Node*> linearNodes;

        std::vector<Skin*> skins;

        std::vector<Texture> textures;
        std::vector<TextureSampler> textureSamplers;
        std::vector<Material> materials;
        std::vector<Animation> animations;
        std::vector<std::string> extensions;

        struct Dimensions {
            glm::vec3 min = glm::vec3(FLT_MAX);
            glm::vec3 max = glm::vec3(-FLT_MAX);
        } dimensions;

        void destroy(VkDevice device)
        {
            if (vertices.buffer != VK_NULL_HANDLE) {
                vkDestroyBuffer(device, vertices.buffer, nullptr);
                vkFreeMemory(device, vertices.memory, nullptr);
            }
            if (indices.buffer != VK_NULL_HANDLE) {
                vkDestroyBuffer(device, indices.buffer, nullptr);
                vkFreeMemory(device, indices.memory, nullptr);
            }
            for (auto texture : textures) {
                texture.destroy();
            }
            textures.resize(0);
            textureSamplers.resize(0);
            for (auto node : nodes) {
                delete node;
            }
            materials.resize(0);
            animations.resize(0);
            nodes.resize(0);
            linearNodes.resize(0);
            extensions.resize(0);
            for (auto skin : skins) {
                delete skin;
            }
            skins.resize(0);
        };

        void loadNode(vkglTF::Node *parent, const tinygltf::Node &node, uint32_t nodeIndex, const tinygltf::Model &model, std::vector<uint32_t>& indexBuffer, std::vector<Vertex>& vertexBuffer, float globalscale)
        {
            vkglTF::Node *newNode = new Node{};
            newNode->index = nodeIndex;
            newNode->parent = parent;
            newNode->name = node.name;
            newNode->skinIndex = node.skin;
            newNode->matrix = glm::mat4(1.0f);

            // Generate local node matrix
            glm::vec3 translation = glm::vec3(0.0f);
            if (node.translation.size() == 3) {
                translation = glm::make_vec3(node.translation.data());
                newNode->translation = translation;
            }
            glm::mat4 rotation = glm::mat4(1.0f);
            if (node.rotation.size() == 4) {
                glm::quat q = glm::make_quat(node.rotation.data());
                newNode->rotation = glm::mat4(q);
            }
            glm::vec3 scale = glm::vec3(1.0f);
            if (node.scale.size() == 3) {
                scale = glm::make_vec3(node.scale.data());
                newNode->scale = scale;
            }
            if (node.matrix.size() == 16) {
                newNode->matrix = glm::make_mat4x4(node.matrix.data());
            };

            // Node with children
            if (node.children.size() > 0) {
                for (size_t i = 0; i < node.children.size(); i++) {
                    loadNode(newNode, model.nodes[node.children[i]], node.children[i], model, indexBuffer, vertexBuffer, globalscale);
                }
            }

            // Node contains mesh data
            if (node.mesh > -1) {
                const tinygltf::Mesh mesh = model.meshes[node.mesh];
                Mesh *newMesh = new Mesh(device, newNode->matrix);
                for (size_t j = 0; j < mesh.primitives.size(); j++) {
                    const tinygltf::Primitive &primitive = mesh.primitives[j];
                    uint32_t indexStart = static_cast<uint32_t>(indexBuffer.size());
                    uint32_t vertexStart = static_cast<uint32_t>(vertexBuffer.size());
                    uint32_t indexCount = 0;
                    uint32_t vertexCount = 0;
                    glm::vec3 posMin{};
                    glm::vec3 posMax{};
                    bool hasSkin = false;
                    bool hasIndices = primitive.indices > -1;
                    // Vertices
                    {
                        const float *bufferPos = nullptr;
                        const float *bufferNormals = nullptr;
                        const float *bufferTexCoordSet0 = nullptr;
                        const float *bufferTexCoordSet1 = nullptr;
                        const uint16_t *bufferJoints = nullptr;
                        const float *bufferWeights = nullptr;

                        int posByteStride;
                        int normByteStride;
                        int uv0ByteStride;
                        int uv1ByteStride;
                        int jointByteStride;
                        int weightByteStride;

                        // Position attribute is required
                        assert(primitive.attributes.find("POSITION") != primitive.attributes.end());

                        const tinygltf::Accessor &posAccessor = model.accessors[primitive.attributes.find("POSITION")->second];
                        const tinygltf::BufferView &posView = model.bufferViews[posAccessor.bufferView];
                        bufferPos = reinterpret_cast<const float *>(&(model.buffers[posView.buffer].data[posAccessor.byteOffset + posView.byteOffset]));
                        posMin = glm::vec3(posAccessor.minValues[0], posAccessor.minValues[1], posAccessor.minValues[2]);
                        posMax = glm::vec3(posAccessor.maxValues[0], posAccessor.maxValues[1], posAccessor.maxValues[2]);
                        vertexCount = static_cast<uint32_t>(posAccessor.count);
                        posByteStride = posAccessor.ByteStride(posView) ? (posAccessor.ByteStride(posView) / sizeof(float)) : tinygltf::GetNumComponentsInType(TINYGLTF_TYPE_VEC3);

                        if (primitive.attributes.find("NORMAL") != primitive.attributes.end()) {
                            const tinygltf::Accessor &normAccessor = model.accessors[primitive.attributes.find("NORMAL")->second];
                            const tinygltf::BufferView &normView = model.bufferViews[normAccessor.bufferView];
                            bufferNormals = reinterpret_cast<const float *>(&(model.buffers[normView.buffer].data[normAccessor.byteOffset + normView.byteOffset]));
                            normByteStride = normAccessor.ByteStride(normView) ? (normAccessor.ByteStride(normView) / sizeof(float)) : tinygltf::GetNumComponentsInType(TINYGLTF_TYPE_VEC3);
                        }

                        if (primitive.attributes.find("TEXCOORD_0") != primitive.attributes.end()) {
                            const tinygltf::Accessor &uvAccessor = model.accessors[primitive.attributes.find("TEXCOORD_0")->second];
                            const tinygltf::BufferView &uvView = model.bufferViews[uvAccessor.bufferView];
                            bufferTexCoordSet0 = reinterpret_cast<const float *>(&(model.buffers[uvView.buffer].data[uvAccessor.byteOffset + uvView.byteOffset]));
                            uv0ByteStride = uvAccessor.ByteStride(uvView) ? (uvAccessor.ByteStride(uvView) / sizeof(float)) : tinygltf::GetNumComponentsInType(TINYGLTF_TYPE_VEC2);
                        }
                        if (primitive.attributes.find("TEXCOORD_1") != primitive.attributes.end()) {
                            const tinygltf::Accessor &uvAccessor = model.accessors[primitive.attributes.find("TEXCOORD_1")->second];
                            const tinygltf::BufferView &uvView = model.bufferViews[uvAccessor.bufferView];
                            bufferTexCoordSet1 = reinterpret_cast<const float *>(&(model.buffers[uvView.buffer].data[uvAccessor.byteOffset + uvView.byteOffset]));
                            uv1ByteStride = uvAccessor.ByteStride(uvView) ? (uvAccessor.ByteStride(uvView) / sizeof(float)) : tinygltf::GetNumComponentsInType(TINYGLTF_TYPE_VEC2);
                        }

                        // Skinning
                        // Joints
                        if (primitive.attributes.find("JOINTS_0") != primitive.attributes.end()) {
                            const tinygltf::Accessor &jointAccessor = model.accessors[primitive.attributes.find("JOINTS_0")->second];
                            const tinygltf::BufferView &jointView = model.bufferViews[jointAccessor.bufferView];
                            bufferJoints = reinterpret_cast<const uint16_t *>(&(model.buffers[jointView.buffer].data[jointAccessor.byteOffset + jointView.byteOffset]));
                            jointByteStride = jointAccessor.ByteStride(jointView) ? (jointAccessor.ByteStride(jointView) / sizeof(bufferJoints[0])) : tinygltf::GetNumComponentsInType(TINYGLTF_TYPE_VEC4);
                        }

                        if (primitive.attributes.find("WEIGHTS_0") != primitive.attributes.end()) {
                            const tinygltf::Accessor &weightAccessor = model.accessors[primitive.attributes.find("WEIGHTS_0")->second];
                            const tinygltf::BufferView &weightView = model.bufferViews[weightAccessor.bufferView];
                            bufferWeights = reinterpret_cast<const float *>(&(model.buffers[weightView.buffer].data[weightAccessor.byteOffset + weightView.byteOffset]));
                            weightByteStride = weightAccessor.ByteStride(weightView) ? (weightAccessor.ByteStride(weightView) / sizeof(float)) : tinygltf::GetNumComponentsInType(TINYGLTF_TYPE_VEC4);
                        }

                        hasSkin = (bufferJoints && bufferWeights);

                        for (size_t v = 0; v < posAccessor.count; v++) {
                            Vertex vert{};
                            vert.pos = glm::vec4(glm::make_vec3(&bufferPos[v * posByteStride]), 1.0f);
                            vert.normal = glm::normalize(glm::vec3(bufferNormals ? glm::make_vec3(&bufferNormals[v * normByteStride]) : glm::vec3(0.0f)));
                            vert.uv0 = bufferTexCoordSet0 ? glm::make_vec2(&bufferTexCoordSet0[v * uv0ByteStride]) : glm::vec3(0.0f);
                            vert.uv1 = bufferTexCoordSet1 ? glm::make_vec2(&bufferTexCoordSet1[v * uv1ByteStride]) : glm::vec3(0.0f);

                            vert.joint0 = hasSkin ? glm::vec4(glm::make_vec4(&bufferJoints[v * jointByteStride])) : glm::vec4(0.0f);
                            vert.weight0 = hasSkin ? glm::make_vec4(&bufferWeights[v * weightByteStride]) : glm::vec4(0.0f);
                            // Fix for all zero weights
                            if (glm::length(vert.weight0) == 0.0f) {
                                vert.weight0 = glm::vec4(1.0f, 0.0f, 0.0f, 0.0f);
                            }
                            vertexBuffer.push_back(vert);
                        }
                    }
                    // Indices
                    if (hasIndices)
                    {
                        const tinygltf::Accessor &accessor = model.accessors[primitive.indices > -1 ? primitive.indices : 0];
                        const tinygltf::BufferView &bufferView = model.bufferViews[accessor.bufferView];
                        const tinygltf::Buffer &buffer = model.buffers[bufferView.buffer];

                        indexCount = static_cast<uint32_t>(accessor.count);
                        const void *dataPtr = &(buffer.data[accessor.byteOffset + bufferView.byteOffset]);

                        switch (accessor.componentType) {
                        case TINYGLTF_PARAMETER_TYPE_UNSIGNED_INT: {
                            const uint32_t *buf = static_cast<const uint32_t*>(dataPtr);
                            for (size_t index = 0; index < accessor.count; index++) {
                                indexBuffer.push_back(buf[index] + vertexStart);
                            }
                            break;
                        }
                        case TINYGLTF_PARAMETER_TYPE_UNSIGNED_SHORT: {
                            const uint16_t *buf = static_cast<const uint16_t*>(dataPtr);
                            for (size_t index = 0; index < accessor.count; index++) {
                                indexBuffer.push_back(buf[index] + vertexStart);
                            }
                            break;
                        }
                        case TINYGLTF_PARAMETER_TYPE_UNSIGNED_BYTE: {
                            const uint8_t *buf = static_cast<const uint8_t*>(dataPtr);
                            for (size_t index = 0; index < accessor.count; index++) {
                                indexBuffer.push_back(buf[index] + vertexStart);
                            }
                            break;
                        }
                        default:
                            std::cout << "Index component type " << accessor.componentType << " not supported!" << std::endl;
                            return;
                        }
                    }                    
                    Primitive *newPrimitive = new Primitive(indexStart, indexCount, vertexCount, primitive.material > -1 ? materials[primitive.material] : materials.back());
                    newPrimitive->setBoundingBox(posMin, posMax);
                    newMesh->primitives.push_back(newPrimitive);
                }
                // Mesh BB from BBs of primitives
                for (auto p : newMesh->primitives) {
                    if (p->bb.valid && !newMesh->bb.valid) {
                        newMesh->bb = p->bb;
                        newMesh->bb.valid = true;
                    }
                    newMesh->bb.min = glm::min(newMesh->bb.min, p->bb.min);
                    newMesh->bb.max = glm::max(newMesh->bb.max, p->bb.max);
                }
                newNode->mesh = newMesh;
            }
            if (parent) {
                parent->children.push_back(newNode);
            } else {
                nodes.push_back(newNode);
            }
            linearNodes.push_back(newNode);
        }

        void loadSkins(tinygltf::Model &gltfModel)
        {
            for (tinygltf::Skin &source : gltfModel.skins) {
                Skin *newSkin = new Skin{};
                newSkin->name = source.name;
                
                // Find skeleton root node
                if (source.skeleton > -1) {
                    newSkin->skeletonRoot = nodeFromIndex(source.skeleton);
                }

                // Find joint nodes
                for (int jointIndex : source.joints) {
                    Node* node = nodeFromIndex(jointIndex);
                    if (node) {
                        newSkin->joints.push_back(nodeFromIndex(jointIndex));
                    }
                }

                // Get inverse bind matrices from buffer
                if (source.inverseBindMatrices > -1) {
                    const tinygltf::Accessor &accessor = gltfModel.accessors[source.inverseBindMatrices];
                    const tinygltf::BufferView &bufferView = gltfModel.bufferViews[accessor.bufferView];
                    const tinygltf::Buffer &buffer = gltfModel.buffers[bufferView.buffer];
                    newSkin->inverseBindMatrices.resize(accessor.count);
                    memcpy(newSkin->inverseBindMatrices.data(), &buffer.data[accessor.byteOffset + bufferView.byteOffset], accessor.count * sizeof(glm::mat4));
                }

                skins.push_back(newSkin);
            }
        }

        void loadTextures(tinygltf::Model &gltfModel, vks::VulkanDevice *device, VkQueue transferQueue)
        {
            for (tinygltf::Texture &tex : gltfModel.textures) {
                tinygltf::Image image = gltfModel.images[tex.source];
                vkglTF::TextureSampler textureSampler;
                if (tex.sampler == -1) {
                    // No sampler specified, use a default one
                    textureSampler.magFilter = VK_FILTER_LINEAR;
                    textureSampler.minFilter = VK_FILTER_LINEAR;
                    textureSampler.addressModeU = VK_SAMPLER_ADDRESS_MODE_REPEAT;
                    textureSampler.addressModeV = VK_SAMPLER_ADDRESS_MODE_REPEAT;
                    textureSampler.addressModeW = VK_SAMPLER_ADDRESS_MODE_REPEAT;
                }
                else {
                    textureSampler = textureSamplers[tex.sampler];
                }
                vkglTF::Texture texture;
                texture.fromglTfImage(image, textureSampler, device, transferQueue);
                textures.push_back(texture);
            }
        }

        VkSamplerAddressMode getVkWrapMode(int32_t wrapMode) 
        {
            switch (wrapMode) {
            case 10497:
                return VK_SAMPLER_ADDRESS_MODE_REPEAT;
            case 33071:
                return VK_SAMPLER_ADDRESS_MODE_CLAMP_TO_EDGE;
            case 33648:
                return VK_SAMPLER_ADDRESS_MODE_MIRRORED_REPEAT;
            }
            return VK_SAMPLER_ADDRESS_MODE_REPEAT;
        }

        VkFilter getVkFilterMode(int32_t filterMode) 
        {
            switch (filterMode) {
            case 9728:
                return VK_FILTER_NEAREST;
            case 9729:
                return VK_FILTER_LINEAR;
            case 9984:
                return VK_FILTER_NEAREST;
            case 9985:
                return VK_FILTER_NEAREST;
            case 9986:
                return VK_FILTER_LINEAR;
            case 9987:
                return VK_FILTER_LINEAR;
            }
            return VK_FILTER_NEAREST;
        }

        void loadTextureSamplers(tinygltf::Model &gltfModel)
        {
            for (tinygltf::Sampler smpl : gltfModel.samplers) {
                vkglTF::TextureSampler sampler{};
                sampler.minFilter = getVkFilterMode(smpl.minFilter);
                sampler.magFilter = getVkFilterMode(smpl.magFilter);
                sampler.addressModeU = getVkWrapMode(smpl.wrapS);
                sampler.addressModeV = getVkWrapMode(smpl.wrapT);
                sampler.addressModeW = sampler.addressModeV;
                textureSamplers.push_back(sampler);
            }
        }

        void loadMaterials(tinygltf::Model &gltfModel)
        {
            for (tinygltf::Material &mat : gltfModel.materials) {
                vkglTF::Material material{};
                if (mat.values.find("baseColorTexture") != mat.values.end()) {
                    material.baseColorTexture = &textures[mat.values["baseColorTexture"].TextureIndex()];
                    material.texCoordSets.baseColor = mat.values["baseColorTexture"].TextureTexCoord();
                }
                if (mat.values.find("metallicRoughnessTexture") != mat.values.end()) {
                    material.metallicRoughnessTexture = &textures[mat.values["metallicRoughnessTexture"].TextureIndex()];
                    material.texCoordSets.metallicRoughness = mat.values["metallicRoughnessTexture"].TextureTexCoord();
                }
                if (mat.values.find("roughnessFactor") != mat.values.end()) {
                    material.roughnessFactor = static_cast<float>(mat.values["roughnessFactor"].Factor());
                }
                if (mat.values.find("metallicFactor") != mat.values.end()) {
                    material.metallicFactor = static_cast<float>(mat.values["metallicFactor"].Factor());
                }
                if (mat.values.find("baseColorFactor") != mat.values.end()) {
                    material.baseColorFactor = glm::make_vec4(mat.values["baseColorFactor"].ColorFactor().data());
                }                
                if (mat.additionalValues.find("normalTexture") != mat.additionalValues.end()) {
                    material.normalTexture = &textures[mat.additionalValues["normalTexture"].TextureIndex()];
                    material.texCoordSets.normal = mat.additionalValues["normalTexture"].TextureTexCoord();
                }
                if (mat.additionalValues.find("emissiveTexture") != mat.additionalValues.end()) {
                    material.emissiveTexture = &textures[mat.additionalValues["emissiveTexture"].TextureIndex()];
                    material.texCoordSets.emissive = mat.additionalValues["emissiveTexture"].TextureTexCoord();
                }
                if (mat.additionalValues.find("occlusionTexture") != mat.additionalValues.end()) {
                    material.occlusionTexture = &textures[mat.additionalValues["occlusionTexture"].TextureIndex()];
                    material.texCoordSets.occlusion = mat.additionalValues["occlusionTexture"].TextureTexCoord();
                }
                if (mat.additionalValues.find("alphaMode") != mat.additionalValues.end()) {
                    tinygltf::Parameter param = mat.additionalValues["alphaMode"];
                    if (param.string_value == "BLEND") {
                        material.alphaMode = Material::ALPHAMODE_BLEND;
                    }
                    if (param.string_value == "MASK") {
                        material.alphaCutoff = 0.5f;
                        material.alphaMode = Material::ALPHAMODE_MASK;
                    }
                }
                if (mat.additionalValues.find("alphaCutoff") != mat.additionalValues.end()) {
                    material.alphaCutoff = static_cast<float>(mat.additionalValues["alphaCutoff"].Factor());
                }
                if (mat.additionalValues.find("emissiveFactor") != mat.additionalValues.end()) {
                    material.emissiveFactor = glm::vec4(glm::make_vec3(mat.additionalValues["emissiveFactor"].ColorFactor().data()), 1.0);
                    material.emissiveFactor = glm::vec4(0.0f);
                }

                // Extensions
                // @TODO: Find out if there is a nicer way of reading these properties with recent tinygltf headers
                if (mat.extensions.find("KHR_materials_pbrSpecularGlossiness") != mat.extensions.end()) {
                    auto ext = mat.extensions.find("KHR_materials_pbrSpecularGlossiness");
                    if (ext->second.Has("specularGlossinessTexture")) {
                        auto index = ext->second.Get("specularGlossinessTexture").Get("index");
                        material.extension.specularGlossinessTexture = &textures[index.Get<int>()];
                        auto texCoordSet = ext->second.Get("specularGlossinessTexture").Get("texCoord");
                        material.texCoordSets.specularGlossiness = texCoordSet.Get<int>();
                        material.pbrWorkflows.specularGlossiness = true;
                    }
                    if (ext->second.Has("diffuseTexture")) {
                        auto index = ext->second.Get("diffuseTexture").Get("index");
                        material.extension.diffuseTexture = &textures[index.Get<int>()];
                    }
                    if (ext->second.Has("diffuseFactor")) {
                        auto factor = ext->second.Get("diffuseFactor");
                        for (uint32_t i = 0; i < factor.ArrayLen(); i++) {
                            auto val = factor.Get(i);
                            material.extension.diffuseFactor[i] = val.IsNumber() ? (float)val.Get<double>() : (float)val.Get<int>();
                        }
                    }
                    if (ext->second.Has("specularFactor")) {
                        auto factor = ext->second.Get("specularFactor");
                        for (uint32_t i = 0; i < factor.ArrayLen(); i++) {
                            auto val = factor.Get(i);
                            material.extension.specularFactor[i] = val.IsNumber() ? (float)val.Get<double>() : (float)val.Get<int>();
                        }
                    }
                }

                materials.push_back(material);
            }
            // Push a default material at the end of the list for meshes with no material assigned
            materials.push_back(Material());
        }

        void loadAnimations(tinygltf::Model &gltfModel)
        {
            for (tinygltf::Animation &anim : gltfModel.animations) {
                vkglTF::Animation animation{};
                animation.name = anim.name;
                if (anim.name.empty()) {
                    animation.name = std::to_string(animations.size());
                }

                // Samplers
                for (auto &samp : anim.samplers) {
                    vkglTF::AnimationSampler sampler{};

                    if (samp.interpolation == "LINEAR") {
                        sampler.interpolation = AnimationSampler::InterpolationType::LINEAR;
                    }
                    if (samp.interpolation == "STEP") {
                        sampler.interpolation = AnimationSampler::InterpolationType::STEP;
                    }
                    if (samp.interpolation == "CUBICSPLINE") {
                        sampler.interpolation = AnimationSampler::InterpolationType::CUBICSPLINE;
                    }

                    // Read sampler input time values
                    {
                        const tinygltf::Accessor &accessor = gltfModel.accessors[samp.input];
                        const tinygltf::BufferView &bufferView = gltfModel.bufferViews[accessor.bufferView];
                        const tinygltf::Buffer &buffer = gltfModel.buffers[bufferView.buffer];

                        assert(accessor.componentType == TINYGLTF_COMPONENT_TYPE_FLOAT);

                        const void *dataPtr = &buffer.data[accessor.byteOffset + bufferView.byteOffset];
                        const float *buf = static_cast<const float*>(dataPtr);
                        for (size_t index = 0; index < accessor.count; index++) {
                            sampler.inputs.push_back(buf[index]);
                        }

                        for (auto input : sampler.inputs) {
                            if (input < animation.start) {
                                animation.start = input;
                            };
                            if (input > animation.end) {
                                animation.end = input;
                            }
                        }
                    }

                    // Read sampler output T/R/S values 
                    {
                        const tinygltf::Accessor &accessor = gltfModel.accessors[samp.output];
                        const tinygltf::BufferView &bufferView = gltfModel.bufferViews[accessor.bufferView];
                        const tinygltf::Buffer &buffer = gltfModel.buffers[bufferView.buffer];

                        assert(accessor.componentType == TINYGLTF_COMPONENT_TYPE_FLOAT);

                        const void *dataPtr = &buffer.data[accessor.byteOffset + bufferView.byteOffset];

                        switch (accessor.type) {
                        case TINYGLTF_TYPE_VEC3: {
                            const glm::vec3 *buf = static_cast<const glm::vec3*>(dataPtr);
                            for (size_t index = 0; index < accessor.count; index++) {
                                sampler.outputsVec4.push_back(glm::vec4(buf[index], 0.0f));
                            }
                            break;
                        }
                        case TINYGLTF_TYPE_VEC4: {
                            const glm::vec4 *buf = static_cast<const glm::vec4*>(dataPtr);
                            for (size_t index = 0; index < accessor.count; index++) {
                                sampler.outputsVec4.push_back(buf[index]);
                            }
                            break;
                        }
                        default: {
                            std::cout << "unknown type" << std::endl;
                            break;
                        }
                        }
                    }

                    animation.samplers.push_back(sampler);
                }

                // Channels
                for (auto &source: anim.channels) {
                    vkglTF::AnimationChannel channel{};

                    if (source.target_path == "rotation") {
                        channel.path = AnimationChannel::PathType::ROTATION;
                    }
                    if (source.target_path == "translation") {
                        channel.path = AnimationChannel::PathType::TRANSLATION;
                    }
                    if (source.target_path == "scale") {
                        channel.path = AnimationChannel::PathType::SCALE;
                    }
                    if (source.target_path == "weights") {
                        std::cout << "weights not yet supported, skipping channel" << std::endl;
                        continue;
                    }
                    channel.samplerIndex = source.sampler;
                    channel.node = nodeFromIndex(source.target_node);
                    if (!channel.node) {
                        continue;
                    }

                    animation.channels.push_back(channel);
                }

                animations.push_back(animation);
            }
        }

        void loadFromFile(std::string filename, vks::VulkanDevice *device, VkQueue transferQueue, float scale = 1.0f)
        {
            tinygltf::Model gltfModel;
            tinygltf::TinyGLTF gltfContext;
            std::string error;
            std::string warning;

            this->name   = filename;
            this->device = device;

            bool binary = false;
            size_t extpos = filename.rfind('.', filename.length());
            if (extpos != std::string::npos) {
                binary = (filename.substr(extpos + 1, filename.length() - extpos) == "glb");
            }  

            bool fileLoaded = binary ? gltfContext.LoadBinaryFromFile(&gltfModel, &error, &warning, filename.c_str()) : gltfContext.LoadASCIIFromFile(&gltfModel, &error, &warning, filename.c_str());
            std::cout << filename << " loaded." << std::endl;

            // Dump(gltfModel);

            for (auto& errmsg : error) {
                std::cout << errmsg << std::endl;
            }
            for (auto& warnmsg : warning) {
                std::cout << warnmsg << std::endl;
            }
            std::vector<uint32_t> indexBuffer;
            std::vector<Vertex> vertexBuffer;

            if (fileLoaded) {
                loadTextureSamplers(gltfModel);
                loadTextures(gltfModel, device, transferQueue);
                loadMaterials(gltfModel);
                // TODO: scene handling with no default scene
                const tinygltf::Scene &scene = gltfModel.scenes[gltfModel.defaultScene > -1 ? gltfModel.defaultScene : 0];
                for (size_t i = 0; i < scene.nodes.size(); i++) {
                    const tinygltf::Node node = gltfModel.nodes[scene.nodes[i]];
                    std::cout << "Begin loadNode" << i <<"..." << scene.nodes.size() << std::endl;
                    loadNode(nullptr, node, scene.nodes[i], gltfModel, indexBuffer, vertexBuffer, scale);
                }
                std::cout << "End loadNode " << scene.nodes.size() << std::endl;
                if (gltfModel.animations.size() > 0) {
                    std::cout << "Begin loadAnimations..." << std::endl;
                    loadAnimations(gltfModel);
                }
                std::cout << "Begin loadSkins..." << std::endl;
                loadSkins(gltfModel);

                for (auto node : linearNodes) {
                    // Assign skins
                    if (node->skinIndex > -1) {
                        node->skin = skins[node->skinIndex];
                    }
                    // Initial pose
                    if (node->mesh) {
                        node->update();
                    }
                }
            }
            else {
                // TODO: throw
                std::cout << "Could not load gltf file: " << error << std::endl;
                return;
            }

            extensions = gltfModel.extensionsUsed;

            size_t vertexBufferSize = vertexBuffer.size() * sizeof(Vertex);
            size_t indexBufferSize = indexBuffer.size() * sizeof(uint32_t);
            indices.count = static_cast<uint32_t>(indexBuffer.size());

            assert(vertexBufferSize > 0);

            struct StagingBuffer {
                VkBuffer buffer;
                VkDeviceMemory memory;
            } vertexStaging, indexStaging;

            // Create staging buffers
            // Vertex data
            VK_CHECK_RESULT(device->createBuffer(
                VK_BUFFER_USAGE_TRANSFER_SRC_BIT,
                VK_MEMORY_PROPERTY_HOST_VISIBLE_BIT | VK_MEMORY_PROPERTY_HOST_COHERENT_BIT,
                vertexBufferSize,
                &vertexStaging.buffer,
                &vertexStaging.memory,
                vertexBuffer.data()));
            // Index data
            if (indexBufferSize > 0) {
                VK_CHECK_RESULT(device->createBuffer(
                    VK_BUFFER_USAGE_TRANSFER_SRC_BIT,
                    VK_MEMORY_PROPERTY_HOST_VISIBLE_BIT | VK_MEMORY_PROPERTY_HOST_COHERENT_BIT,
                    indexBufferSize,
                    &indexStaging.buffer,
                    &indexStaging.memory,
                    indexBuffer.data()));
            }

            // Create device local buffers
            // Vertex buffer
            VK_CHECK_RESULT(device->createBuffer(
                VK_BUFFER_USAGE_VERTEX_BUFFER_BIT | VK_BUFFER_USAGE_TRANSFER_DST_BIT,
                VK_MEMORY_PROPERTY_DEVICE_LOCAL_BIT,
                vertexBufferSize,
                &vertices.buffer,
                &vertices.memory));
            // Index buffer
            if (indexBufferSize > 0) {
                VK_CHECK_RESULT(device->createBuffer(
                    VK_BUFFER_USAGE_INDEX_BUFFER_BIT | VK_BUFFER_USAGE_TRANSFER_DST_BIT,
                    VK_MEMORY_PROPERTY_DEVICE_LOCAL_BIT,
                    indexBufferSize,
                    &indices.buffer,
                    &indices.memory));
            }

            // Copy from staging buffers
            VkCommandBuffer copyCmd = device->createCommandBuffer(VK_COMMAND_BUFFER_LEVEL_PRIMARY, true);

            VkBufferCopy copyRegion = {};

            copyRegion.size = vertexBufferSize;
            vkCmdCopyBuffer(copyCmd, vertexStaging.buffer, vertices.buffer, 1, &copyRegion);

            if (indexBufferSize > 0) {
                copyRegion.size = indexBufferSize;
                vkCmdCopyBuffer(copyCmd, indexStaging.buffer, indices.buffer, 1, &copyRegion);
            }

            device->flushCommandBuffer(copyCmd, transferQueue, true);

            vkDestroyBuffer(device->logicalDevice, vertexStaging.buffer, nullptr);
            vkFreeMemory(device->logicalDevice, vertexStaging.memory, nullptr);
            if (indexBufferSize > 0) {
                vkDestroyBuffer(device->logicalDevice, indexStaging.buffer, nullptr);
                vkFreeMemory(device->logicalDevice, indexStaging.memory, nullptr);
            }

            getSceneDimensions();
        }

        void drawNode(Node *node, VkCommandBuffer commandBuffer)
        {
            if (node->mesh) {
                for (Primitive *primitive : node->mesh->primitives) {
                    vkCmdDrawIndexed(commandBuffer, primitive->indexCount, 1, primitive->firstIndex, 0, 0);
                }
            }
            for (auto& child : node->children) {
                drawNode(child, commandBuffer);
            }
        }

        void draw(VkCommandBuffer commandBuffer)
        {
            const VkDeviceSize offsets[1] = { 0 };
            vkCmdBindVertexBuffers(commandBuffer, 0, 1, &vertices.buffer, offsets);
            vkCmdBindIndexBuffer(commandBuffer, indices.buffer, 0, VK_INDEX_TYPE_UINT32);
            for (auto& node : nodes) {
                drawNode(node, commandBuffer);
            }
        }

        void calculateBoundingBox(Node *node, Node *parent) {
            BoundingBox parentBvh = parent ? parent->bvh : BoundingBox(dimensions.min, dimensions.max);

            if (node->mesh) {
                if (node->mesh->bb.valid) {
                    node->aabb = node->mesh->bb.getAABB(node->getMatrix());
                    if (node->children.size() == 0) {
                        node->bvh.min = node->aabb.min;
                        node->bvh.max = node->aabb.max;
                        node->bvh.valid = true;
                    }
                }
            }

            parentBvh.min = glm::min(parentBvh.min, node->bvh.min);
            parentBvh.max = glm::min(parentBvh.max, node->bvh.max);

            for (auto &child : node->children) {
                calculateBoundingBox(child, node);
            }
        }

        void getSceneDimensions()
        {
            // Calculate binary volume hierarchy for all nodes in the scene
            for (auto node : linearNodes) {
                calculateBoundingBox(node, nullptr);
            }

            dimensions.min = glm::vec3(FLT_MAX);
            dimensions.max = glm::vec3(-FLT_MAX);

            for (auto node : linearNodes) {
                if (node->bvh.valid) {
                    dimensions.min = glm::min(dimensions.min, node->bvh.min);
                    dimensions.max = glm::max(dimensions.max, node->bvh.max);
                }
            }

            // Calculate scene aabb
            aabb = glm::scale(glm::mat4(1.0f), glm::vec3(dimensions.max[0] - dimensions.min[0], dimensions.max[1] - dimensions.min[1], dimensions.max[2] - dimensions.min[2]));
            aabb[3][0] = dimensions.min[0];
            aabb[3][1] = dimensions.min[1];
            aabb[3][2] = dimensions.min[2];
        }

        void updateAnimation(uint32_t index, float time) 
        {
            if (animations.empty()) {
                std::cout << ".glTF does not contain animation." << std::endl;
                return;
            }
            if (index > static_cast<uint32_t>(animations.size()) - 1) {
                std::cout << "No animation with index " << index << std::endl;
                return;
            }
            Animation &animation = animations[index];

            bool updated = false;
            for (auto& channel : animation.channels) {
                vkglTF::AnimationSampler &sampler = animation.samplers[channel.samplerIndex];
                if (sampler.inputs.size() > sampler.outputsVec4.size()) {
                    continue;
                }

                for (size_t i = 0; i < sampler.inputs.size() - 1; i++) {
                    if ((time >= sampler.inputs[i]) && (time <= sampler.inputs[i + 1])) {
                        float u = std::max(0.0f, time - sampler.inputs[i]) / (sampler.inputs[i + 1] - sampler.inputs[i]);
                        if (u <= 1.0f) {
                            switch (channel.path) {
                            case vkglTF::AnimationChannel::PathType::TRANSLATION: {
                                glm::vec4 trans = glm::mix(sampler.outputsVec4[i], sampler.outputsVec4[i + 1], u);
                                channel.node->translation = glm::vec3(trans);
                                break;
                            }
                            case vkglTF::AnimationChannel::PathType::SCALE: {
                                glm::vec4 trans = glm::mix(sampler.outputsVec4[i], sampler.outputsVec4[i + 1], u);
                                channel.node->scale = glm::vec3(trans);
                                break;
                            }
                            case vkglTF::AnimationChannel::PathType::ROTATION: {
                                glm::quat q1;
                                q1.x = sampler.outputsVec4[i].x;
                                q1.y = sampler.outputsVec4[i].y;
                                q1.z = sampler.outputsVec4[i].z;
                                q1.w = sampler.outputsVec4[i].w;
                                glm::quat q2;
                                q2.x = sampler.outputsVec4[i + 1].x;
                                q2.y = sampler.outputsVec4[i + 1].y;
                                q2.z = sampler.outputsVec4[i + 1].z;
                                q2.w = sampler.outputsVec4[i + 1].w;
                                channel.node->rotation = glm::normalize(glm::slerp(q1, q2, u));
                                break;
                            }
                            }
                            updated = true;
                        }
                    }
                }
            }
            if (updated) {
                for (auto &node : nodes) {
                    node->update();
                }
            }
        }

        /*
            Helper functions
        */
        Node* findNode(Node *parent, uint32_t index) {
            Node* nodeFound = nullptr;
            if (parent->index == index) {
                return parent;
            }
            for (auto& child : parent->children) {
                nodeFound = findNode(child, index);
                if (nodeFound) {
                    break;
                }
            }
            return nodeFound;
        }

        Node* nodeFromIndex(uint32_t index) {
            Node* nodeFound = nullptr;
            for (auto &node : nodes) {
                nodeFound = findNode(node, index);
                if (nodeFound) {
                    break;
                }
            }
            return nodeFound;
        }
    };

    static std::string PrintMode(int mode)
    {
        if (mode == TINYGLTF_MODE_POINTS) {
            return "POINTS";
        } else if (mode == TINYGLTF_MODE_LINE) {
            return "LINE";
        } else if (mode == TINYGLTF_MODE_LINE_LOOP) {
            return "LINE_LOOP";
        } else if (mode == TINYGLTF_MODE_TRIANGLES) {
            return "TRIANGLES";
        } else if (mode == TINYGLTF_MODE_TRIANGLE_FAN) {
            return "TRIANGLE_FAN";
        } else if (mode == TINYGLTF_MODE_TRIANGLE_STRIP) {
            return "TRIANGLE_STRIP";
        }
        return "**UNKNOWN**";
    }

    static std::string PrintTarget(int target)
    {
        if (target == 34962) {
            return "GL_ARRAY_BUFFER";
        } else if (target == 34963) {
            return "GL_ELEMENT_ARRAY_BUFFER";
        } else {
            return "**UNKNOWN**";
        }
    }

    static std::string PrintType(int ty)
    {
        if (ty == TINYGLTF_TYPE_SCALAR) {
            return "SCALAR";
        } else if (ty == TINYGLTF_TYPE_VECTOR) {
            return "VECTOR";
        } else if (ty == TINYGLTF_TYPE_VEC2) {
            return "VEC2";
        } else if (ty == TINYGLTF_TYPE_VEC3) {
            return "VEC3";
        } else if (ty == TINYGLTF_TYPE_VEC4) {
            return "VEC4";
        } else if (ty == TINYGLTF_TYPE_MATRIX) {
            return "MATRIX";
        } else if (ty == TINYGLTF_TYPE_MAT2) {
            return "MAT2";
        } else if (ty == TINYGLTF_TYPE_MAT3) {
            return "MAT3";
        } else if (ty == TINYGLTF_TYPE_MAT4) {
            return "MAT4";
        }
        return "**UNKNOWN**";
    }

    static std::string PrintComponentType(int ty)
    {
        if (ty == TINYGLTF_COMPONENT_TYPE_BYTE) {
            return "BYTE";
        } else if (ty == TINYGLTF_COMPONENT_TYPE_UNSIGNED_BYTE) {
            return "UNSIGNED_BYTE";
        } else if (ty == TINYGLTF_COMPONENT_TYPE_SHORT) {
            return "SHORT";
        } else if (ty == TINYGLTF_COMPONENT_TYPE_UNSIGNED_SHORT) {
            return "UNSIGNED_SHORT";
        } else if (ty == TINYGLTF_COMPONENT_TYPE_INT) {
            return "INT";
        } else if (ty == TINYGLTF_COMPONENT_TYPE_UNSIGNED_INT) {
            return "UNSIGNED_INT";
        } else if (ty == TINYGLTF_COMPONENT_TYPE_FLOAT) {
            return "FLOAT";
        } else if (ty == TINYGLTF_COMPONENT_TYPE_DOUBLE) {
            return "DOUBLE";
        }

        return "**UNKNOWN**";
    }

#if 0
    static std::string PrintParameterType(int ty)
    {
        if (ty == TINYGLTF_PARAMETER_TYPE_BYTE) {
            return "BYTE";
        } else if (ty == TINYGLTF_PARAMETER_TYPE_UNSIGNED_BYTE) {
            return "UNSIGNED_BYTE";
        } else if (ty == TINYGLTF_PARAMETER_TYPE_SHORT) {
            return "SHORT";
        } else if (ty == TINYGLTF_PARAMETER_TYPE_UNSIGNED_SHORT) {
            return "UNSIGNED_SHORT";
        } else if (ty == TINYGLTF_PARAMETER_TYPE_INT) {
            return "INT";
        } else if (ty == TINYGLTF_PARAMETER_TYPE_UNSIGNED_INT) {
            return "UNSIGNED_INT";
        } else if (ty == TINYGLTF_PARAMETER_TYPE_FLOAT) {
            return "FLOAT";
        } else if (ty == TINYGLTF_PARAMETER_TYPE_FLOAT_VEC2) {
            return "FLOAT_VEC2";
        } else if (ty == TINYGLTF_PARAMETER_TYPE_FLOAT_VEC3) {
            return "FLOAT_VEC3";
        } else if (ty == TINYGLTF_PARAMETER_TYPE_FLOAT_VEC4) {
            return "FLOAT_VEC4";
        } else if (ty == TINYGLTF_PARAMETER_TYPE_INT_VEC2) {
            return "INT_VEC2";
        } else if (ty == TINYGLTF_PARAMETER_TYPE_INT_VEC3) {
            return "INT_VEC3";
        } else if (ty == TINYGLTF_PARAMETER_TYPE_INT_VEC4) {
            return "INT_VEC4";
        } else if (ty == TINYGLTF_PARAMETER_TYPE_BOOL) {
            return "BOOL";
        } else if (ty == TINYGLTF_PARAMETER_TYPE_BOOL_VEC2) {
            return "BOOL_VEC2";
        } else if (ty == TINYGLTF_PARAMETER_TYPE_BOOL_VEC3) {
            return "BOOL_VEC3";
        } else if (ty == TINYGLTF_PARAMETER_TYPE_BOOL_VEC4) {
            return "BOOL_VEC4";
        } else if (ty == TINYGLTF_PARAMETER_TYPE_FLOAT_MAT2) {
            return "FLOAT_MAT2";
        } else if (ty == TINYGLTF_PARAMETER_TYPE_FLOAT_MAT3) {
            return "FLOAT_MAT3";
        } else if (ty == TINYGLTF_PARAMETER_TYPE_FLOAT_MAT4) {
            return "FLOAT_MAT4";
        } else if (ty == TINYGLTF_PARAMETER_TYPE_SAMPLER_2D) {
            return "SAMPLER_2D";
        }

        return "**UNKNOWN**";
    }
#endif

    static std::string PrintWrapMode(int mode)
    {
        if (mode == TINYGLTF_TEXTURE_WRAP_REPEAT) {
            return "REPEAT";
        } else if (mode == TINYGLTF_TEXTURE_WRAP_CLAMP_TO_EDGE) {
            return "CLAMP_TO_EDGE";
        } else if (mode == TINYGLTF_TEXTURE_WRAP_MIRRORED_REPEAT) {
            return "MIRRORED_REPEAT";
        }

        return "**UNKNOWN**";
    }

    static std::string PrintFilterMode(int mode)
    {
        if (mode == TINYGLTF_TEXTURE_FILTER_NEAREST) {
            return "NEAREST";
        } else if (mode == TINYGLTF_TEXTURE_FILTER_LINEAR) {
            return "LINEAR";
        } else if (mode == TINYGLTF_TEXTURE_FILTER_NEAREST_MIPMAP_NEAREST) {
            return "NEAREST_MIPMAP_NEAREST";
        } else if (mode == TINYGLTF_TEXTURE_FILTER_NEAREST_MIPMAP_LINEAR) {
            return "NEAREST_MIPMAP_LINEAR";
        } else if (mode == TINYGLTF_TEXTURE_FILTER_LINEAR_MIPMAP_NEAREST) {
            return "LINEAR_MIPMAP_NEAREST";
        } else if (mode == TINYGLTF_TEXTURE_FILTER_LINEAR_MIPMAP_LINEAR) {
            return "LINEAR_MIPMAP_LINEAR";
        }
        return "**UNKNOWN**";
    }

    static std::string PrintIntArray(const std::vector<int> &arr)
    {
        if (arr.size() == 0) {
            return "";
        }

        std::stringstream ss;
        ss << "[ ";
        for (size_t i = 0; i < arr.size(); i++) {
            ss << arr[i];
            if (i != arr.size() - 1) {
            ss << ", ";
            }
        }
        ss << " ]";

        return ss.str();
    }

    static std::string PrintFloatArray(const std::vector<double> &arr)
    {
        if (arr.size() == 0) {
            return "";
        }

        std::stringstream ss;
        ss << "[ ";
        for (size_t i = 0; i < arr.size(); i++) {
            ss << arr[i];
            if (i != arr.size() - 1) {
            ss << ", ";
            }
        }
        ss << " ]";

        return ss.str();
    }

    static std::string Indent(const int indent)
    {
        std::string s;
        for (int i = 0; i < indent; i++) {
            s += "  ";
        }

        return s;
    }

    static std::string PrintParameterValue(const tinygltf::Parameter &param)
    {
        if (!param.number_array.empty()) {
            return PrintFloatArray(param.number_array);
        } else {
            return param.string_value;
        }
    }

#if 0
    static std::string PrintParameterMap(const tinygltf::ParameterMap &pmap)
    {
        std::stringstream ss;

        ss << pmap.size() << std::endl;
        for (auto &kv : pmap) {
            ss << kv.first << " : " << PrintParameterValue(kv.second) << std::endl;
        }

        return ss.str();
    }
#endif

    static std::string PrintValue(const std::string &name,
                                const tinygltf::Value &value, const int indent,
                                const bool tag = true)
    {
        std::stringstream ss;

        if (value.IsObject()) {
            const tinygltf::Value::Object &o = value.Get<tinygltf::Value::Object>();
            tinygltf::Value::Object::const_iterator it(o.begin());
            tinygltf::Value::Object::const_iterator itEnd(o.end());
            for (; it != itEnd; it++) {
            ss << PrintValue(it->first, it->second, indent + 1) << std::endl;
            }
        } else if (value.IsString()) {
            if (tag) {
            ss << Indent(indent) << name << " : " << value.Get<std::string>();
            } else {
            ss << Indent(indent) << value.Get<std::string>() << " ";
            }
        } else if (value.IsBool()) {
            if (tag) {
            ss << Indent(indent) << name << " : " << value.Get<bool>();
            } else {
            ss << Indent(indent) << value.Get<bool>() << " ";
            }
        } else if (value.IsNumber()) {
            if (tag) {
            ss << Indent(indent) << name << " : " << value.Get<double>();
            } else {
            ss << Indent(indent) << value.Get<double>() << " ";
            }
        } else if (value.IsInt()) {
            if (tag) {
            ss << Indent(indent) << name << " : " << value.Get<int>();
            } else {
            ss << Indent(indent) << value.Get<int>() << " ";
            }
        } else if (value.IsArray()) {
            // TODO(syoyo): Better pretty printing of array item
            ss << Indent(indent) << name << " [ \n";
            for (size_t i = 0; i < value.Size(); i++) {
            ss << PrintValue("", value.Get(int(i)), indent + 1, /* tag */ false);
            if (i != (value.ArrayLen() - 1)) {
                ss << ", \n";
            }
            }
            ss << "\n" << Indent(indent) << "] ";
        }

        // @todo { binary }

        return ss.str();
    }

    static void DumpNode(const tinygltf::Node &node, int indent)
    {
        std::cout << Indent(indent) << "name        : " << node.name << std::endl;
        std::cout << Indent(indent) << "camera      : " << node.camera << std::endl;
        std::cout << Indent(indent) << "mesh        : " << node.mesh << std::endl;
        if (!node.rotation.empty()) {
            std::cout << Indent(indent)
                    << "rotation    : " << PrintFloatArray(node.rotation)
                    << std::endl;
        }
        if (!node.scale.empty()) {
            std::cout << Indent(indent)
                    << "scale       : " << PrintFloatArray(node.scale) << std::endl;
        }
        if (!node.translation.empty()) {
            std::cout << Indent(indent)
                    << "translation : " << PrintFloatArray(node.translation)
                    << std::endl;
        }

        if (!node.matrix.empty()) {
            std::cout << Indent(indent)
                    << "matrix      : " << PrintFloatArray(node.matrix) << std::endl;
        }

        std::cout << Indent(indent)
                    << "children    : " << PrintIntArray(node.children) << std::endl;
    }

    static void DumpStringIntMap(const std::map<std::string, int> &m, int indent)
    {
        std::map<std::string, int>::const_iterator it(m.begin());
        std::map<std::string, int>::const_iterator itEnd(m.end());
        for (; it != itEnd; it++) {
            std::cout << Indent(indent) << it->first << ": " << it->second << std::endl;
        }
    }

    static void DumpExtensions(const tinygltf::ExtensionMap &extension,
                            const int indent)
    {
        // TODO(syoyo): pritty print Value
        for (auto &e : extension) {
            std::cout << Indent(indent) << e.first << std::endl;
            std::cout << PrintValue("extensions", e.second, indent + 1) << std::endl;
        }
    }

    static void DumpPrimitive(const tinygltf::Primitive &primitive, int indent)
    {
        std::cout << Indent(indent) << "material : " << primitive.material
                    << std::endl;
        std::cout << Indent(indent) << "indices : " << primitive.indices << std::endl;
        std::cout << Indent(indent) << "mode     : " << PrintMode(primitive.mode)
                    << "(" << primitive.mode << ")" << std::endl;
        std::cout << Indent(indent)
                    << "attributes(items=" << primitive.attributes.size() << ")"
                    << std::endl;
        DumpStringIntMap(primitive.attributes, indent + 1);

        DumpExtensions(primitive.extensions, indent);
        std::cout << Indent(indent) << "extras :" << std::endl
                    << PrintValue("extras", primitive.extras, indent + 1) << std::endl;

        if (!primitive.extensions_json_string.empty()) {
            std::cout << Indent(indent + 1) << "extensions(JSON string) = "
                    << primitive.extensions_json_string << "\n";
        }

        if (!primitive.extras_json_string.empty()) {
            std::cout << Indent(indent + 1)
                    << "extras(JSON string) = " << primitive.extras_json_string
                    << "\n";
        }
    }

    static void DumpTextureInfo(const tinygltf::TextureInfo &texinfo,
                                const int indent)
    {
        std::cout << Indent(indent) << "index     : " << texinfo.index << "\n";
        std::cout << Indent(indent) << "texCoord  : TEXCOORD_" << texinfo.texCoord
                    << "\n";
        DumpExtensions(texinfo.extensions, indent + 1);
        std::cout << PrintValue("extras", texinfo.extras, indent + 1) << "\n";

        if (!texinfo.extensions_json_string.empty()) {
            std::cout << Indent(indent)
                    << "extensions(JSON string) = " << texinfo.extensions_json_string
                    << "\n";
        }

        if (!texinfo.extras_json_string.empty()) {
            std::cout << Indent(indent)
                    << "extras(JSON string) = " << texinfo.extras_json_string << "\n";
        }
    }

static void DumpNormalTextureInfo(const tinygltf::NormalTextureInfo &texinfo,
                                  const int indent)
    {
        std::cout << Indent(indent) << "index     : " << texinfo.index << "\n";
        std::cout << Indent(indent) << "texCoord  : TEXCOORD_" << texinfo.texCoord
                    << "\n";
        std::cout << Indent(indent) << "scale     : " << texinfo.scale << "\n";
        DumpExtensions(texinfo.extensions, indent + 1);
        std::cout << PrintValue("extras", texinfo.extras, indent + 1) << "\n";
    }

static void DumpOcclusionTextureInfo(
    const tinygltf::OcclusionTextureInfo &texinfo, const int indent)
    {
        std::cout << Indent(indent) << "index     : " << texinfo.index << "\n";
        std::cout << Indent(indent) << "texCoord  : TEXCOORD_" << texinfo.texCoord
                    << "\n";
        std::cout << Indent(indent) << "strength  : " << texinfo.strength << "\n";
        DumpExtensions(texinfo.extensions, indent + 1);
        std::cout << PrintValue("extras", texinfo.extras, indent + 1) << "\n";
    }

    static void DumpPbrMetallicRoughness(const tinygltf::PbrMetallicRoughness &pbr,
                                        const int indent)
    {
        std::cout << Indent(indent)
                    << "baseColorFactor   : " << PrintFloatArray(pbr.baseColorFactor)
                    << "\n";
        std::cout << Indent(indent) << "baseColorTexture  :\n";
        DumpTextureInfo(pbr.baseColorTexture, indent + 1);

        std::cout << Indent(indent) << "metallicFactor    : " << pbr.metallicFactor
                    << "\n";
        std::cout << Indent(indent) << "roughnessFactor   : " << pbr.roughnessFactor
                    << "\n";

        std::cout << Indent(indent) << "metallicRoughnessTexture  :\n";
        DumpTextureInfo(pbr.metallicRoughnessTexture, indent + 1);
        DumpExtensions(pbr.extensions, indent + 1);
        std::cout << PrintValue("extras", pbr.extras, indent + 1) << "\n";
    }

    static void Dump(const tinygltf::Model &model)
    {
        std::cout << "=== Dump glTF ===" << std::endl;
        std::cout << "asset.copyright          : " << model.asset.copyright
                    << std::endl;
        std::cout << "asset.generator          : " << model.asset.generator
                    << std::endl;
        std::cout << "asset.version            : " << model.asset.version
                    << std::endl;
        std::cout << "asset.minVersion         : " << model.asset.minVersion
                    << std::endl;
        std::cout << std::endl;

        std::cout << "=== Dump scene ===" << std::endl;
        std::cout << "defaultScene: " << model.defaultScene << std::endl;

        {
            std::cout << "scenes(items=" << model.scenes.size() << ")" << std::endl;
            for (size_t i = 0; i < model.scenes.size(); i++) {
            std::cout << Indent(1) << "scene[" << i
                        << "] name  : " << model.scenes[i].name << std::endl;
            DumpExtensions(model.scenes[i].extensions, 1);
            }
        }

        {
            std::cout << "meshes(item=" << model.meshes.size() << ")" << std::endl;
            for (size_t i = 0; i < model.meshes.size(); i++) {
            std::cout << Indent(1) << "name     : " << model.meshes[i].name
                        << std::endl;
            std::cout << Indent(1)
                        << "primitives(items=" << model.meshes[i].primitives.size()
                        << "): " << std::endl;

            for (size_t k = 0; k < model.meshes[i].primitives.size(); k++) {
                DumpPrimitive(model.meshes[i].primitives[k], 2);
            }
            }
        }

        {
            for (size_t i = 0; i < model.accessors.size(); i++) {
            const tinygltf::Accessor &accessor = model.accessors[i];
            std::cout << Indent(1) << "name         : " << accessor.name << std::endl;
            std::cout << Indent(2) << "bufferView   : " << accessor.bufferView
                        << std::endl;
            std::cout << Indent(2) << "byteOffset   : " << accessor.byteOffset
                        << std::endl;
            std::cout << Indent(2) << "componentType: "
                        << PrintComponentType(accessor.componentType) << "("
                        << accessor.componentType << ")" << std::endl;
            std::cout << Indent(2) << "count        : " << accessor.count
                        << std::endl;
            std::cout << Indent(2) << "type         : " << PrintType(accessor.type)
                        << std::endl;
            if (!accessor.minValues.empty()) {
                std::cout << Indent(2) << "min          : [";
                for (size_t k = 0; k < accessor.minValues.size(); k++) {
                std::cout << accessor.minValues[k]
                            << ((k != accessor.minValues.size() - 1) ? ", " : "");
                }
                std::cout << "]" << std::endl;
            }
            if (!accessor.maxValues.empty()) {
                std::cout << Indent(2) << "max          : [";
                for (size_t k = 0; k < accessor.maxValues.size(); k++) {
                std::cout << accessor.maxValues[k]
                            << ((k != accessor.maxValues.size() - 1) ? ", " : "");
                }
                std::cout << "]" << std::endl;
            }

            if (accessor.sparse.isSparse) {
                std::cout << Indent(2) << "sparse:" << std::endl;
                std::cout << Indent(3) << "count  : " << accessor.sparse.count
                        << std::endl;
                std::cout << Indent(3) << "indices: " << std::endl;
                std::cout << Indent(4)
                        << "bufferView   : " << accessor.sparse.indices.bufferView
                        << std::endl;
                std::cout << Indent(4)
                        << "byteOffset   : " << accessor.sparse.indices.byteOffset
                        << std::endl;
                std::cout << Indent(4) << "componentType: "
                        << PrintComponentType(accessor.sparse.indices.componentType)
                        << "(" << accessor.sparse.indices.componentType << ")"
                        << std::endl;
                std::cout << Indent(3) << "values : " << std::endl;
                std::cout << Indent(4)
                        << "bufferView   : " << accessor.sparse.values.bufferView
                        << std::endl;
                std::cout << Indent(4)
                        << "byteOffset   : " << accessor.sparse.values.byteOffset
                        << std::endl;
            }
            }
        }

        {
            std::cout << "animations(items=" << model.animations.size() << ")"
                    << std::endl;
            for (size_t i = 0; i < model.animations.size(); i++) {
            const tinygltf::Animation &animation = model.animations[i];
            std::cout << Indent(1) << "name         : " << animation.name
                        << std::endl;

            std::cout << Indent(1) << "channels : [ " << std::endl;
            for (size_t j = 0; i < animation.channels.size(); i++) {
                std::cout << Indent(2)
                        << "sampler     : " << animation.channels[j].sampler
                        << std::endl;
                std::cout << Indent(2)
                        << "target.id   : " << animation.channels[j].target_node
                        << std::endl;
                std::cout << Indent(2)
                        << "target.path : " << animation.channels[j].target_path
                        << std::endl;
                std::cout << ((i != (animation.channels.size() - 1)) ? "  , " : "");
            }
            std::cout << "  ]" << std::endl;

            std::cout << Indent(1) << "samplers(items=" << animation.samplers.size()
                        << ")" << std::endl;
            for (size_t j = 0; j < animation.samplers.size(); j++) {
                const tinygltf::AnimationSampler &sampler = animation.samplers[j];
                std::cout << Indent(2) << "input         : " << sampler.input
                        << std::endl;
                std::cout << Indent(2) << "interpolation : " << sampler.interpolation
                        << std::endl;
                std::cout << Indent(2) << "output        : " << sampler.output
                        << std::endl;
            }
            }
        }

        {
            std::cout << "bufferViews(items=" << model.bufferViews.size() << ")"
                    << std::endl;
            for (size_t i = 0; i < model.bufferViews.size(); i++) {
            const tinygltf::BufferView &bufferView = model.bufferViews[i];
            std::cout << Indent(1) << "name         : " << bufferView.name
                        << std::endl;
            std::cout << Indent(2) << "buffer       : " << bufferView.buffer
                        << std::endl;
            std::cout << Indent(2) << "byteLength   : " << bufferView.byteLength
                        << std::endl;
            std::cout << Indent(2) << "byteOffset   : " << bufferView.byteOffset
                        << std::endl;
            std::cout << Indent(2) << "byteStride   : " << bufferView.byteStride
                        << std::endl;
            std::cout << Indent(2)
                        << "target       : " << PrintTarget(bufferView.target)
                        << std::endl;
            std::cout << Indent(1) << "-------------------------------------\n";

            DumpExtensions(bufferView.extensions, 1);
            std::cout << PrintValue("extras", bufferView.extras, 2) << std::endl;

            if (!bufferView.extensions_json_string.empty()) {
                std::cout << Indent(2) << "extensions(JSON string) = "
                        << bufferView.extensions_json_string << "\n";
            }

            if (!bufferView.extras_json_string.empty()) {
                std::cout << Indent(2)
                        << "extras(JSON string) = " << bufferView.extras_json_string
                        << "\n";
            }
            }
        }

        {
            std::cout << "buffers(items=" << model.buffers.size() << ")" << std::endl;
            for (size_t i = 0; i < model.buffers.size(); i++) {
            const tinygltf::Buffer &buffer = model.buffers[i];
            std::cout << Indent(1) << "name         : " << buffer.name << std::endl;
            std::cout << Indent(2) << "byteLength   : " << buffer.data.size()
                        << std::endl;
            std::cout << Indent(1) << "-------------------------------------\n";

            DumpExtensions(buffer.extensions, 1);
            std::cout << PrintValue("extras", buffer.extras, 2) << std::endl;

            if (!buffer.extensions_json_string.empty()) {
                std::cout << Indent(2) << "extensions(JSON string) = "
                        << buffer.extensions_json_string << "\n";
            }

            if (!buffer.extras_json_string.empty()) {
                std::cout << Indent(2)
                        << "extras(JSON string) = " << buffer.extras_json_string
                        << "\n";
            }
            }
        }

        {
            std::cout << "materials(items=" << model.materials.size() << ")"
                    << std::endl;
            for (size_t i = 0; i < model.materials.size(); i++) {
            const tinygltf::Material &material = model.materials[i];
            std::cout << Indent(1) << "name                 : " << material.name
                        << std::endl;

            std::cout << Indent(1) << "alphaMode            : " << material.alphaMode
                        << std::endl;
            std::cout << Indent(1)
                        << "alphaCutoff          : " << material.alphaCutoff
                        << std::endl;
            std::cout << Indent(1) << "doubleSided          : "
                        << (material.doubleSided ? "true" : "false") << std::endl;
            std::cout << Indent(1) << "emissiveFactor       : "
                        << PrintFloatArray(material.emissiveFactor) << std::endl;

            std::cout << Indent(1) << "pbrMetallicRoughness :\n";
            DumpPbrMetallicRoughness(material.pbrMetallicRoughness, 2);

            std::cout << Indent(1) << "normalTexture        :\n";
            DumpNormalTextureInfo(material.normalTexture, 2);

            std::cout << Indent(1) << "occlusionTexture     :\n";
            DumpOcclusionTextureInfo(material.occlusionTexture, 2);

            std::cout << Indent(1) << "emissiveTexture      :\n";
            DumpTextureInfo(material.emissiveTexture, 2);

            std::cout << Indent(1) << "----  legacy material parameter  ----\n";
            std::cout << Indent(1) << "values(items=" << material.values.size() << ")"
                        << std::endl;
            tinygltf::ParameterMap::const_iterator p(material.values.begin());
            tinygltf::ParameterMap::const_iterator pEnd(material.values.end());
            for (; p != pEnd; p++) {
                std::cout << Indent(2) << p->first << ": "
                        << PrintParameterValue(p->second) << std::endl;
            }
            std::cout << Indent(1) << "-------------------------------------\n";

            DumpExtensions(material.extensions, 1);
            std::cout << PrintValue("extras", material.extras, 2) << std::endl;

            if (!material.extensions_json_string.empty()) {
                std::cout << Indent(2) << "extensions(JSON string) = "
                        << material.extensions_json_string << "\n";
            }

            if (!material.extras_json_string.empty()) {
                std::cout << Indent(2)
                        << "extras(JSON string) = " << material.extras_json_string
                        << "\n";
            }
            }
        }

        {
            std::cout << "nodes(items=" << model.nodes.size() << ")" << std::endl;
            for (size_t i = 0; i < model.nodes.size(); i++) {
            const tinygltf::Node &node = model.nodes[i];
            std::cout << Indent(1) << "name         : " << node.name << std::endl;

            DumpNode(node, 2);
            }
        }

        {
            std::cout << "images(items=" << model.images.size() << ")" << std::endl;
            for (size_t i = 0; i < model.images.size(); i++) {
            const tinygltf::Image &image = model.images[i];
            std::cout << Indent(1) << "name         : " << image.name << std::endl;

            std::cout << Indent(2) << "width     : " << image.width << std::endl;
            std::cout << Indent(2) << "height    : " << image.height << std::endl;
            std::cout << Indent(2) << "component : " << image.component << std::endl;
            DumpExtensions(image.extensions, 1);
            std::cout << PrintValue("extras", image.extras, 2) << std::endl;

            if (!image.extensions_json_string.empty()) {
                std::cout << Indent(2) << "extensions(JSON string) = "
                        << image.extensions_json_string << "\n";
            }

            if (!image.extras_json_string.empty()) {
                std::cout << Indent(2)
                        << "extras(JSON string) = " << image.extras_json_string
                        << "\n";
            }
            }
        }

        {
            std::cout << "textures(items=" << model.textures.size() << ")" << std::endl;
            for (size_t i = 0; i < model.textures.size(); i++) {
            const tinygltf::Texture &texture = model.textures[i];
            std::cout << Indent(1) << "sampler        : " << texture.sampler
                        << std::endl;
            std::cout << Indent(1) << "source         : " << texture.source
                        << std::endl;
            DumpExtensions(texture.extensions, 1);
            std::cout << PrintValue("extras", texture.extras, 2) << std::endl;

            if (!texture.extensions_json_string.empty()) {
                std::cout << Indent(2) << "extensions(JSON string) = "
                        << texture.extensions_json_string << "\n";
            }

            if (!texture.extras_json_string.empty()) {
                std::cout << Indent(2)
                        << "extras(JSON string) = " << texture.extras_json_string
                        << "\n";
            }
            }
        }

        {
            std::cout << "samplers(items=" << model.samplers.size() << ")" << std::endl;

            for (size_t i = 0; i < model.samplers.size(); i++) {
            const tinygltf::Sampler &sampler = model.samplers[i];
            std::cout << Indent(1) << "name (id)    : " << sampler.name << std::endl;
            std::cout << Indent(2)
                        << "minFilter    : " << PrintFilterMode(sampler.minFilter)
                        << std::endl;
            std::cout << Indent(2)
                        << "magFilter    : " << PrintFilterMode(sampler.magFilter)
                        << std::endl;
            std::cout << Indent(2)
                        << "wrapS        : " << PrintWrapMode(sampler.wrapS)
                        << std::endl;
            std::cout << Indent(2)
                        << "wrapT        : " << PrintWrapMode(sampler.wrapT)
                        << std::endl;

            DumpExtensions(sampler.extensions, 1);
            std::cout << PrintValue("extras", sampler.extras, 2) << std::endl;

            if (!sampler.extensions_json_string.empty()) {
                std::cout << Indent(2) << "extensions(JSON string) = "
                        << sampler.extensions_json_string << "\n";
            }

            if (!sampler.extras_json_string.empty()) {
                std::cout << Indent(2)
                        << "extras(JSON string) = " << sampler.extras_json_string
                        << "\n";
            }
            }
        }

        {
            std::cout << "cameras(items=" << model.cameras.size() << ")" << std::endl;

            for (size_t i = 0; i < model.cameras.size(); i++) {
            const tinygltf::Camera &camera = model.cameras[i];
            std::cout << Indent(1) << "name (id)    : " << camera.name << std::endl;
            std::cout << Indent(1) << "type         : " << camera.type << std::endl;

            if (camera.type.compare("perspective") == 0) {
                std::cout << Indent(2)
                        << "aspectRatio   : " << camera.perspective.aspectRatio
                        << std::endl;
                std::cout << Indent(2) << "yfov          : " << camera.perspective.yfov
                        << std::endl;
                std::cout << Indent(2) << "zfar          : " << camera.perspective.zfar
                        << std::endl;
                std::cout << Indent(2) << "znear         : " << camera.perspective.znear
                        << std::endl;
            } else if (camera.type.compare("orthographic") == 0) {
                std::cout << Indent(2) << "xmag          : " << camera.orthographic.xmag
                        << std::endl;
                std::cout << Indent(2) << "ymag          : " << camera.orthographic.ymag
                        << std::endl;
                std::cout << Indent(2) << "zfar          : " << camera.orthographic.zfar
                        << std::endl;
                std::cout << Indent(2)
                        << "znear         : " << camera.orthographic.znear
                        << std::endl;
            }

            std::cout << Indent(1) << "-------------------------------------\n";

            DumpExtensions(camera.extensions, 1);
            std::cout << PrintValue("extras", camera.extras, 2) << std::endl;

            if (!camera.extensions_json_string.empty()) {
                std::cout << Indent(2) << "extensions(JSON string) = "
                        << camera.extensions_json_string << "\n";
            }

            if (!camera.extras_json_string.empty()) {
                std::cout << Indent(2)
                        << "extras(JSON string) = " << camera.extras_json_string
                        << "\n";
            }
            }
        }

        {
            std::cout << "skins(items=" << model.skins.size() << ")" << std::endl;
            for (size_t i = 0; i < model.skins.size(); i++) {
            const tinygltf::Skin &skin = model.skins[i];
            std::cout << Indent(1) << "name         : " << skin.name << std::endl;
            std::cout << Indent(2)
                        << "inverseBindMatrices   : " << skin.inverseBindMatrices
                        << std::endl;
            std::cout << Indent(2) << "skeleton              : " << skin.skeleton
                        << std::endl;
            std::cout << Indent(2)
                        << "joints                : " << PrintIntArray(skin.joints)
                        << std::endl;
            std::cout << Indent(1) << "-------------------------------------\n";

            DumpExtensions(skin.extensions, 1);
            std::cout << PrintValue("extras", skin.extras, 2) << std::endl;

            if (!skin.extensions_json_string.empty()) {
                std::cout << Indent(2)
                        << "extensions(JSON string) = " << skin.extensions_json_string
                        << "\n";
            }

            if (!skin.extras_json_string.empty()) {
                std::cout << Indent(2)
                        << "extras(JSON string) = " << skin.extras_json_string
                        << "\n";
            }
            }
        }

        // toplevel extensions
        {
            std::cout << "extensions(items=" << model.extensions.size() << ")"
                    << std::endl;
            DumpExtensions(model.extensions, 1);
        }
    }
}
